// 繁忙的都市
// 一个非常繁忙的大都市，城市中的道路十分的拥挤，于是市长决定对其中的道路进行改造
// 城市的道路是这样分布的：城市中有n个交叉路口，有些交叉路口之间有道路相连
// 两个交叉路口之间最多有一条道路相连接，这些道路是双向的
// 且把所有的交叉路口直接或间接的连接起来了
// 每条道路都有一个分值，分值越小表示这个道路越繁忙，越需要进行改造
// 但是市政府的资金有限，市长希望进行改造的道路越少越好，于是他提出下面的要求：
// 1. 改造的那些道路能够把所有的交叉路口直接或间接的连通起来
// 2. 在满足要求1的情况下，改造的道路尽量少
// 3. 在满足要求1、2的情况下，改造的那些道路中分值最大的道路分值尽量小
// 作为市规划局的你，应当作出最佳的决策，选择哪些道路应当被修建
// 返回选出了几条道路 以及 分值最大的那条道路的分值是多少
// 测试链接 : https://www.luogu.com.cn/problem/P2330
// 提交以下所有代码，，可以直接通过

// 本题考察的是瓶颈生成树（最小瓶颈树）
// 无向图 G 的瓶颈生成树是这样的一个生成树，它的最大的边权值在 G 的所有生成树中最小。
// 性质：最小生成树是瓶颈生成树的充分不必要条件。 即最小生成树一定是瓶颈生成树，而
// 瓶颈生成树不一定是最小生成树。

// 证明：
// 关于最小生成树一定是瓶颈生成树这一命题，可以运用反证法证明：我们设最小生成树中的最
// 大边权为 w，如果最小生成树不是瓶颈生成树的话，则瓶颈生成树的所有边权都小于 w，我
// 们只需删去原最小生成树中的最长边，用瓶颈生成树中的一条边来连接删去边后形成的两棵树，
// 得到的新生成树一定比原最小生成树的权值和还要小，这样就产生了矛盾。


#include <tuple>
#include <algorithm>
#include <iostream>

using namespace std;

const int MAXN = 301;
const int MAXM = 8001;

int father[MAXN];
// 0 : u, 1 : v, 2 : w
tuple<int, int, int> edges[MAXM];

int n, m;

void Build(int n)
{
    for(int i = 1; i <= n; ++i)
    {
        father[i] = i;
    }
}

int Find(int i)
{
    return i == father[i] ? i : father[i] = Find(father[i]);
}

// 如果 x 和 y 本来就是一个集合，返回 false
// 如果 x 和 y 不是一个集合，合并之后返回 true
bool Union(int x, int y)
{
    int fx = Find(x);
    int fy = Find(y);
    if(fx != fy)
    {
        father[fx] = fy;
        return true;
    }
    return false;
}

int main()
{
    cin >> n >> m;
    Build(n);
    for(int i = 0; i < m; ++i)
    {
        // 0 : u, 1 : v, 2 : w
        cin >> get<0>(edges[i]) >> get<1>(edges[i]) >> get<2>(edges[i]);
    }
    sort(edges, edges + m, [](auto& a, auto& b){
        return get<2>(a) < get<2>(b); // 根据 weight 来排序
    });

    int ans = 0;
    int edgeCnt = 0;
    // 跑 Kruskal 算法
    for(auto& [u, v, w] : edges)
    {
        if(Union(u, v))
        {
            ++edgeCnt;
            ans = max(ans, w);
        }
        if(edgeCnt == n - 1) break;
    }
    cout << n - 1 << " " << ans << endl;

    return 0;
}